package store

import (
	"context"
	"fmt"
	"strconv"

	"github.com/cortezaproject/corteza-server/compose/types"
	"github.com/cortezaproject/corteza-server/pkg/envoy"
	"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
	"github.com/cortezaproject/corteza-server/pkg/filter"
	"github.com/cortezaproject/corteza-server/store"
)

type (
	composeModuleState struct {
		cfg *EncoderConfig

		res *resource.ComposeModule
		mod *types.Module

		relNS     *types.Namespace
		recFields map[string]uint64
	}
)

func NewComposeModuleState(res *resource.ComposeModule, cfg *EncoderConfig) resourceState {
	return &composeModuleState{
		cfg: mergeConfig(cfg, res.Config()),

		res: res,

		recFields: make(map[string]uint64),
	}
}

func (n *composeModuleState) Prepare(ctx context.Context, s store.Storer, state *envoy.ResourceState) (err error) {
	// Initial values
	if n.res.Res.CreatedAt.IsZero() {
		n.res.Res.CreatedAt = *now()
	}

	// Get relate namespace
	n.relNS, err = findComposeNamespaceRS(ctx, s, state.ParentResources, n.res.NsRef.Identifiers)
	if err != nil {
		return err
	}
	if n.relNS == nil {
		return composeNamespaceErrUnresolved(n.res.NsRef.Identifiers)
	}

	// Get related record field modules
	for _, r := range n.res.ModRef {
		var mod *types.Module
		if n.relNS.ID > 0 {
			mod, err = findComposeModuleS(ctx, s, n.relNS.ID, makeGenericFilter(r.Identifiers))
			if err != nil {
				return err
			}
		}
		if mod == nil {
			mod = findComposeModuleR(state.ParentResources, r.Identifiers)
		}
		if mod == nil {
			return composeModuleErrUnresolvedRecordField(r.Identifiers)
		}

		for i := range r.Identifiers {
			n.recFields[i] = mod.ID
		}
	}

	// Can't do anything else, since the NS doesn't yet exist
	if n.relNS.ID <= 0 {
		return nil
	}

	// Try to get the original module
	n.mod, err = findComposeModuleS(ctx, s, n.relNS.ID, makeGenericFilter(n.res.Identifiers()))
	if err != nil {
		return err
	}

	// Nothing else to do
	if n.mod == nil {
		return nil
	}

	// Get the original module fields
	// These are used later for some merging logic
	n.mod.Fields, err = findComposeModuleFieldsS(ctx, s, n.mod)
	if err != nil {
		return err
	}

	if n.mod != nil {
		n.res.Res.ID = n.mod.ID
		n.res.Res.NamespaceID = n.mod.NamespaceID
	}
	return nil
}

func (n *composeModuleState) Encode(ctx context.Context, s store.Storer, state *envoy.ResourceState) (err error) {
	res := n.res.Res
	exists := n.mod != nil && n.mod.ID > 0

	// Determine the ID
	if res.ID <= 0 && exists {
		res.ID = n.mod.ID
	}
	if res.ID <= 0 {
		res.ID = NextID()
	}

	if state.Conflicting {
		return nil
	}

	// Timestamps
	ts := n.res.Timestamps()
	if ts != nil {
		if ts.CreatedAt != "" {
			t := toTime(ts.CreatedAt)
			if t != nil {
				res.CreatedAt = *t
			}
		}
		if ts.UpdatedAt != "" {
			res.UpdatedAt = toTime(ts.UpdatedAt)
		}
		if ts.DeletedAt != "" {
			res.DeletedAt = toTime(ts.DeletedAt)
		}
	}

	// Namespace
	res.NamespaceID = n.relNS.ID
	if res.NamespaceID <= 0 {
		ns := findComposeNamespaceR(state.ParentResources, n.res.NsRef.Identifiers)
		res.NamespaceID = ns.ID
	}

	if res.NamespaceID <= 0 {
		return composeNamespaceErrUnresolved(n.res.NsRef.Identifiers)
	}

	// Fields
	off := make(types.ModuleFieldSet, 0)
	if n.mod != nil && n.mod.Fields != nil {
		off = n.mod.Fields
	}
	for i, f := range res.Fields {
		of := off.FindByName(f.Name)
		if of != nil {
			f.ID = of.ID
		} else {
			f.ID = NextID()
		}
		f.ModuleID = res.ID
		f.Place = i
		f.DeletedAt = nil
		f.CreatedAt = *now()

		if f.Kind == "Record" {
			refM := f.Options.String("module")
			mID := n.recFields[refM]
			if mID <= 0 {
				ii := resource.MakeIdentifiers(refM)
				mod := findComposeModuleR(state.ParentResources, ii)
				if mod == nil || mod.ID <= 0 {
					return composeModuleErrUnresolvedRecordField(ii)
				}
				mID = mod.ID
			}

			f.Options["moduleID"] = strconv.FormatUint(mID, 10)
			delete(f.Options, "module")
		}
	}

	// Evaluate the resource skip expression
	// @todo expand available parameters; similar implementation to compose/types/record@Dict
	if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
		return err
	} else if skip {
		return nil
	}

	// Create a fresh module
	if !exists {
		err = store.CreateComposeModule(ctx, s, res)
		if err != nil {
			return err
		}

		err = store.CreateComposeModuleField(ctx, s, res.Fields...)
		if err != nil {
			return err
		}

		return nil
	}

	// Update existing module
	switch n.cfg.OnExisting {
	case resource.Skip:
		return nil

	case resource.MergeLeft:
		res = mergeComposeModule(n.mod, res)
		res.Fields = mergeComposeModuleFields(n.mod.Fields, res.Fields)

	case resource.MergeRight:
		res = mergeComposeModule(res, n.mod)
		res.Fields = mergeComposeModuleFields(res.Fields, n.mod.Fields)
	}

	err = store.UpdateComposeModule(ctx, s, res)
	if err != nil {
		return err
	}

	err = store.DeleteComposeModuleField(ctx, s, n.mod.Fields...)
	if err != nil {
		return err
	}
	err = store.CreateComposeModuleField(ctx, s, res.Fields...)
	if err != nil {
		return err
	}

	n.res.Res = res
	return nil
}

// mergeComposeModuleFields merges b into a, prioritising a
func mergeComposeModuleFields(a, b types.ModuleFieldSet) types.ModuleFieldSet {
	ff := a.Clone()
	missing := make(types.ModuleFieldSet, 0, len(b))

	for _, fb := range b {
		for _, fa := range ff {
			if fb.ID != fa.ID && fb.Name != fa.Name {
				continue
			}

			fa.ModuleID = fb.ModuleID
			fa.Place = fb.Place
			if fa.Kind == "" {
				fa.Kind = fb.Kind
			}
			if fa.Name == "" {
				fa.Name = fb.Name
			}
			if fa.Label == "" {
				fa.Label = fb.Label
			}
			if fa.Options == nil {
				fa.Options = fb.Options
			}
			if fa.DefaultValue == nil {
				fa.DefaultValue = fb.DefaultValue
			}
			if fa.Expressions.ValueExpr == "" {
				fa.Expressions.ValueExpr = fb.Expressions.ValueExpr
			}
			if len(fa.Expressions.Sanitizers) == 0 {
				fa.Expressions.Sanitizers = fb.Expressions.Sanitizers
			}
			if len(fa.Expressions.Validators) == 0 {
				fa.Expressions.Validators = fb.Expressions.Validators
			}
			if len(fa.Expressions.Formatters) == 0 {
				fa.Expressions.Formatters = fb.Expressions.Formatters
			}

			if fa.CreatedAt.IsZero() {
				fa.CreatedAt = fb.CreatedAt
			}
			if fa.UpdatedAt == nil {
				fa.UpdatedAt = fb.UpdatedAt
			}
			if fa.DeletedAt == nil {
				fa.DeletedAt = fb.DeletedAt
			}

			goto out
		}
		missing = append(missing, fb)
	out:
	}

	ff = append(ff, missing...)

	return ff
}

// mergeComposeModule merges b into a, prioritising a
func mergeComposeModule(a, b *types.Module) *types.Module {
	c := a.Clone()

	if c.Handle == "" {
		c.Handle = b.Handle
	}
	if c.Name == "" {
		c.Name = b.Name
	}
	c.NamespaceID = b.NamespaceID

	if c.CreatedAt.IsZero() {
		c.CreatedAt = b.CreatedAt
	}
	if c.UpdatedAt == nil {
		c.UpdatedAt = b.UpdatedAt
	}
	if c.DeletedAt == nil {
		c.DeletedAt = b.DeletedAt
	}

	// I'll just compare the entire struct for now
	if c.Meta == nil {
		c.Meta = b.Meta
	}

	return c
}

// findComposeModuleRS looks for the chart in the resources & the store
//
// Provided resources are prioritized.
func findComposeModuleRS(ctx context.Context, s store.Storer, nsID uint64, rr resource.InterfaceSet, ii resource.Identifiers) (mod *types.Module, err error) {
	mod = findComposeModuleR(rr, ii)
	if mod != nil {
		return mod, nil
	}

	if nsID <= 0 {
		return nil, nil
	}

	// Go in the store
	return findComposeModuleS(ctx, s, nsID, makeGenericFilter(ii))
}

// findComposeModuleS looks for the module in the store
func findComposeModuleS(ctx context.Context, s store.Storer, nsID uint64, gf genericFilter) (mod *types.Module, err error) {
	if nsID == 0 {
		return nil, nil
	}

	if gf.id > 0 {
		mod, err = store.LookupComposeModuleByID(ctx, s, gf.id)
		if err != nil && err != store.ErrNotFound {
			return nil, err
		}

		if mod != nil {
			return
		}
	}

	for _, i := range gf.identifiers {
		mod, err = store.LookupComposeModuleByNamespaceIDHandle(ctx, s, nsID, i)
		if err == store.ErrNotFound {
			var mm types.ModuleSet
			mm, _, err = store.SearchComposeModules(ctx, s, types.ModuleFilter{
				NamespaceID: nsID,
				Name:        i,
				Paging: filter.Paging{
					Limit: 2,
				},
			})
			if len(mm) > 1 {
				return nil, resourceErrIdentifierNotUnique(i)
			}
			if len(mm) == 1 {
				mod = mm[0]
			}
		}

		if err != nil && err != store.ErrNotFound {
			return nil, err
		}

		if mod != nil {
			return
		}
	}

	return nil, nil
}

// findComposeModuleR looks for the module in the store
func findComposeModuleR(rr resource.InterfaceSet, ii resource.Identifiers) (ns *types.Module) {
	var modRes *resource.ComposeModule

	rr.Walk(func(r resource.Interface) error {
		mr, ok := r.(*resource.ComposeModule)
		if !ok {
			return nil
		}

		if mr.Identifiers().HasAny(ii) {
			modRes = mr
		}
		return nil
	})

	// Found it
	if modRes != nil {
		return modRes.Res
	}
	return nil
}

// findComposeModuleFieldsS looks for the module fields in the store
func findComposeModuleFieldsS(ctx context.Context, s store.Storer, mod *types.Module) (types.ModuleFieldSet, error) {
	if mod.ID <= 0 {
		return mod.Fields, nil
	}

	ff, _, err := store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{
		ModuleID: []uint64{mod.ID},
	})
	if err != nil {
		return nil, err
	}

	return ff, nil
}

func composeModuleErrUnresolved(ii resource.Identifiers) error {
	return fmt.Errorf("compose module unresolved %v", ii.StringSlice())
}

func composeModuleErrUnresolvedRecordField(ii resource.Identifiers) error {
	return fmt.Errorf("record module field unresolved %v", ii.StringSlice())
}
